package edu.utdallas.cs.securechat.main;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
import java.util.Date;
import java.util.Vector;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import edu.utdallas.cs.securechat.ui.ChatPanel;
import edu.utdallas.cs.securechat.utils.Logger;

public class Client {
	private String serverDomainName = "";
	private boolean loggedIn = false;
	private ClientProtocol cp = new ClientProtocol();
	private Key serverPubKey = null;
	private ServerSocket ss = null;
	public Vector<String> onlineUsers = new Vector<String>();
	private String username = null;

	public Client() {
		Thread messageWaitingThread = new MessageWaitingThread();
		messageWaitingThread.start();
	}

	public void setServerDomainName(String serverDomainName) {
		this.serverDomainName = serverDomainName;
	}

	public void getServerPublicKey() {
		Key serverPubKey = null;
		Message requestKeyMessage = new Message("request_public_key");
		Message reply = sendMessageToServer(requestKeyMessage);
		serverPubKey = reply.getKey();
		this.serverPubKey = serverPubKey;
		Logger.log(Logger.DEBUG, "Public Key: " + this.serverPubKey.toString());
	}

	public String getServerDomainName() {
		return serverDomainName;
	}

	public boolean isLoggedIn() {
		return loggedIn;
	}

	public void setLoggedIn(boolean loggedIn) {
		this.loggedIn = loggedIn;
	}
	
	public String getUserName(){
		return username;
	}
	
	public void setUserName(String username){
		this.username = username;
	}
	
	private Message sendMessageToServer(Message m) {
		Socket s = null;
		ObjectInputStream in = null;
		ObjectOutputStream out = null;

		try {
			s = new Socket(serverDomainName, 12000);
			Logger.log(Logger.DEBUG, s.toString());
			out = new ObjectOutputStream(s.getOutputStream());
			out.writeObject(m);
			Logger.log(Logger.DEBUG, "Wrote message: " + m);
			in = new ObjectInputStream(s.getInputStream());
			Message reply = (Message) in.readObject();
			return reply;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	private Message sendMessageToClient(Message m, String username) {
		Socket s = null;
		ObjectInputStream in = null;
		ObjectOutputStream out = null;

		try {
			s = new Socket(cp.userSessions.get(username).getAddress(), 12345);
			Logger.log(Logger.DEBUG, s.toString());
			out = new ObjectOutputStream(s.getOutputStream());
			out.writeObject(m);
			Logger.log(Logger.DEBUG, "Wrote message: " + m);
			in = new ObjectInputStream(s.getInputStream());
			Message reply = (Message) in.readObject();
			return reply;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}

	public Message register(String user, char[] pass) throws InvalidKeyException, NoSuchAlgorithmException,
			NoSuchProviderException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeySpecException {
		cp.setUsername(user);
		cp.keyFromPassword(pass);
		byte[] encryptedPassword = cp.RSAEncrypt(convertToByteArray(pass), serverPubKey);
		Message registerMessage = new Message("register");
		registerMessage.setUsername(user);
		registerMessage.setData(encryptedPassword);
		Message reply = sendMessageToServer(registerMessage);
		return reply;
	}

	public Message login(String user, char[] pass) throws InvalidKeyException, NoSuchAlgorithmException,
			NoSuchProviderException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeySpecException {
		cp.setUsername(user);
		cp.keyFromPassword(pass);
		byte[] encryptedPassword = cp.RSAEncrypt(convertToByteArray(pass), serverPubKey);
		Message loginMessage = new Message("login");
		loginMessage.setUsername(user);
		loginMessage.setData(encryptedPassword);
		Message reply = sendMessageToServer(loginMessage);
		if(reply.getOperationStatus()){
			Logger.log(Logger.DEBUG, "Data: "+reply.getData());
			try {
				cp.HandleTGT(reply.getData());
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return reply;
	}
	
	public void clientExit(){
		Message clientExitMessage = new Message("client_exit");
		clientExitMessage.setUsername(username);
		sendMessageToServer(clientExitMessage);
	}
	
	public Message requestTicket(String user){
		Message request = new Message("request_ticket");
		long timestamp = new Date().getTime();
		try {
			request.setData(cp.requestTicket(user, timestamp));
			request.setUsername(user);
			Message result = sendMessageToServer(request);
			cp.handleTicketResponse(result.getData(), timestamp);
			cp.userSessions.get(user).setAddress(result.getAddress());
			return result;
		} catch (Exception e) {
			e.printStackTrace();
			return request;
		}
		
	}
	
	public Message connectToUser(String user){
		Message msgToUser = new Message("session_request");
		long timestamp = new Date().getTime();
		try {
			msgToUser.setData(cp.authenticateToClient(user, timestamp));
			msgToUser.setUsername(cp.getUsername());
			msgToUser.setKey(cp.getPublicKey());
			Message reply = sendMessageToClient(msgToUser, user);
			cp.confirmUserConnection(reply.getData(), reply.getUsername(), reply.getKey(), timestamp);
			return reply;
		} catch (Exception e){
			e.printStackTrace();
		}
		return msgToUser;
	}
	
	public Message messageToUser(String user, String text){
		Message msgToUser = new Message("message_from_client");
		try{
			msgToUser.setData(cp.encryptMessage(cp.signMessage(text.getBytes()), user));
			msgToUser.setUsername(cp.getUsername());
			Message reply = sendMessageToClient(msgToUser, user);
			return reply;
		} catch (Exception e){
			e.printStackTrace();
			return msgToUser;
		}
	}

	private byte[] convertToByteArray(char[] pass) {
		byte[] bytes = new byte[pass.length * 2];
		for (int i = 0; i < pass.length; i++) {
			bytes[i * 2] = (byte) (pass[i] >> 8);
			bytes[i * 2 + 1] = (byte) pass[i];
		}
		return bytes;
	}
	
	private class MessageWaitingThread extends Thread{
		public void run(){
			try {
				// create new server socket at port 12345, and wait for client
				// connection
				ss = new ServerSocket(12345, 10);
				Logger.log(Logger.DEBUG, "Starting client messager receiver...");
				while (true) {
					Logger.log(Logger.INFO, "Waiting for connection...");
					new ConnectionReceiver(ss.accept());
				}
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				try {
					ss.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	private class ConnectionReceiver extends Thread {
		Socket s = null;
		ObjectInputStream in = null;
		ObjectOutputStream out = null;
		Message incomingMessage = null;
		Message outGoingMessage = null;
		public ConnectionReceiver(Socket s) {
			this.s = s;
			start();
		}

		public void run() {
			Logger.log(Logger.INFO, "Recieved: " + s);
			try {
				in = new ObjectInputStream(s.getInputStream());
				out = new ObjectOutputStream(s.getOutputStream());
				incomingMessage = (Message) in.readObject();
				Logger.log(Logger.DEBUG, incomingMessage.toString());
				/*if (incomingMessage.getOperation().equals("update_users")) {
					onlineUsers = incomingMessage.getOnlineUsers();
					Logger.log(Logger.DEBUG, "Online Users: "+onlineUsers);
					ChatPanel.updateUserList(onlineUsers);
					outGoingMessage = new Message("ack");
					outGoingMessage.setOperationStatus(true);
					out.writeObject(outGoingMessage);
				}*/
				if(incomingMessage.getOperation().equals("user_logged_on")){
					onlineUsers = incomingMessage.getOnlineUsers();
					Logger.log(Logger.DEBUG, "Online Users: "+onlineUsers);
					if(username != null){
						if(!incomingMessage.getUsername().equals(username))
							ChatPanel.addUser(incomingMessage.getUsername());
					}
					ChatPanel.updateUserList(onlineUsers);
					outGoingMessage = new Message("ack");
					outGoingMessage.setOperationStatus(true);
					out.writeObject(outGoingMessage);
				} else if(incomingMessage.getOperation().equals("user_logged_off")){
					onlineUsers = incomingMessage.getOnlineUsers();
					Logger.log(Logger.DEBUG, "Online Users: "+onlineUsers);
					ChatPanel.removeUser(incomingMessage.getUsername());
					ChatPanel.updateUserList(onlineUsers);
					outGoingMessage = new Message("ack");
					outGoingMessage.setOperationStatus(true);
					out.writeObject(outGoingMessage);
				} else if (incomingMessage.getOperation().equals("session_request")){
					outGoingMessage = new Message("confirm_session");
					try{
						outGoingMessage.setData(cp.checkUserAuthentication(incomingMessage.getData(), incomingMessage.getKey()));
						outGoingMessage.setOperationStatus(true);
						outGoingMessage.setUsername(cp.getUsername());
						outGoingMessage.setKey(cp.getPublicKey());
						cp.setUserAddress(incomingMessage.getUsername(), s.getInetAddress());
					} catch (Exception e){
						e.printStackTrace();
						outGoingMessage.setOperationStatus(false);
						outGoingMessage.setStatusMessage(e.getMessage());
					}
					out.writeObject(outGoingMessage);
				} else if (incomingMessage.getOperation().equals("message_from_client")){
					outGoingMessage = new Message("ack");
					try{
						String username = incomingMessage.getUsername();
						byte[] message = cp.checkMessage(cp.decryptMessage(incomingMessage.getData(), username), username);
						ChatPanel.recievedMessage(username, new String(message));
					} catch (Exception e) {
						Logger.log(Logger.DEBUG, "Error decrypting message from" + incomingMessage.getUsername());
					}
					out.writeObject(outGoingMessage);
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
}
